package msg;

import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

import registration.RegistrationManager;
import user.User;

import common.AbstractManager;
import common.dal.Transaction;
import common.exceptions.*;
import common.interfaces.*;

/**
 * A <i> Singleton </i> class that Manages all messages, event messages and event registrations
 * 
 * @author Gal Lalouche
 */
public class MessageManager extends AbstractManager implements IDAL<Message, Long> {
	private static AtomicLong					counter;
	private final IMessagePersistencyManager	persistencyManager;
	
	private MessageManager(Transaction t) {
		super(t);
		persistencyManager = new DBMessagePersistencyManager(t);
		
		try {
			if (counter == null) {
				counter = new AtomicLong(persistencyManager.getLastId() + 1);
			}
		} catch (SQLException e) {
			// happens because messaging is empty, it is safe to assume counter is 0
			counter = new AtomicLong();
		}
	}
	
	/**
	 * @param t The transaction all queries from this DB will be under
	 * @return The instance
	 */
	public static MessageManager getNewInstance(Transaction t) {
		return new MessageManager(t);
	}
	
	/**
	 * Creates a new messages with a unique id
	 * 
	 * @param location The message's location
	 * @param content The message's content
	 * @param poster The message's poster
	 * @return The new message
	 * @throws InvalidUserException If the poster is not registered to the system
	 * @throws SQLException
	 */
	public Message createNewMessage(Location location, String content, User poster) throws InvalidUserException,
			SQLException {
		Message message = new Message(counter.getAndIncrement(), location, content, poster);
		persistencyManager.store(message);
		return message;
	}
	
	// private Transaction setNewTransaction() throws SQLException {
	// Transaction $ = new Transaction();
	// persistencyManager.setTransaction($);
	// return $;
	// }
	
	/**
	 * Creates a new event with a unique id
	 * 
	 * @param location The event's location
	 * @param content The event's content
	 * @param poster The event's poster
	 * @param time The event's time in unix time
	 * @param capacity The event's maximum capacity
	 * @return The new event
	 * @throws InvalidUserException If the poster is not registered to the system
	 * @throws SQLException
	 */
	public EventMessage createNewEventMessage(Location location, String content, User poster, long time,
			int capacity) throws InvalidUserException, SQLException {
		if (time < new Date().getTime()) {
			throw new ArithmeticException("time has already passed");
		}
		EventMessage event = new EventMessage(counter.getAndIncrement(), location, content, poster, time, capacity);
		// is instantiated before check in order to create an object for the exception if needed
		persistencyManager.store(event);
		return event;
	}
	
	/**
	 * Creates a new messages with a unique id
	 * 
	 * @param location The event's location
	 * @param content The event's content
	 * @param poster The event's poster
	 * @param time The event's time
	 * @param capacity The event's maximum capacity
	 * @return The new event
	 * @throws InvalidUserException If the poster is not registered to the system
	 * @throws SQLException
	 */
	public EventMessage createNewEventMessage(Location location, String content, User poster, Calendar time,
			int capacity) throws InvalidUserException, SQLException {
		return createNewEventMessage(location, content, poster, time.getTimeInMillis(), capacity);
	}
	
	/**
	 * Clears all persisted data
	 * 
	 * @throws SQLException
	 */
	@Override
	public void clear() throws SQLException {
		persistencyManager.clear();
		counter.set(0);
		logMethod();
	}
	
	/**
	 * Gets all messages and events posted by a given user
	 * 
	 * @param user The user to check
	 * @return All messages posted by the user
	 * @throws InvalidUserException
	 * @throws SQLException
	 */
	public Collection<Message> getAllUserPosts(User user) throws SQLException, InvalidUserException {
		return persistencyManager.getAllUserPosts(user);
	}
	
	/**
	 * Gets all events posted by a given user
	 * 
	 * @param user The user to check
	 * @param since Only show messages newer than this; use <code>null</code> for none
	 * @param includePastEvents If true, past events (that are already over) will be included
	 * @return All events posted by the user
	 * @throws InvalidUserException
	 * @throws SQLException
	 */
	public Collection<EventMessage> getallUserEvents(User user, Long since, boolean includePastEvents)
			throws InvalidUserException, SQLException {
		return persistencyManager.getAllUserEvents(user, since, includePastEvents);
	}
	
	/**
	 * Gets all messages (but not events) posted by the user
	 * 
	 * @param user The user to check
	 * @param since
	 * @return Messages only posted by the user (not events)
	 * @throws InvalidUserException
	 * @throws SQLException
	 */
	public Collection<Message> getallUserMessagesOnly(User user, Long since) throws InvalidUserException,
			SQLException {
		return persistencyManager.getAllUserMessagesOnly(user, since);
	}
	
	/**
	 * Removes all messages and events posted by the user
	 * 
	 * @param user The user
	 * @throws InvalidUserException If the user is not registered to they system
	 * @throws SQLException
	 */
	public void removeAllUserMessagesAndEvents(User user) throws SQLException, InvalidUserException {
		persistencyManager.removeAllUserMessagesAndEvents(user);
	}
	
	/**
	 * Sets up the system; <b> will delete all data </b>
	 * 
	 * @throws SQLException
	 */
	@Override
	public void setup() throws SQLException {
		persistencyManager.setup();
		clear();
		logMethod();
	}
	
	/**
	 * @return {@code true} iff a table was dropped
	 * @throws SQLException
	 */
	@Override
	public boolean drop() throws SQLException {
		boolean reslt = persistencyManager.drop();
		logMethod();
		return reslt;
	}
	
	@Override
	public boolean exists(Long key) throws SQLException {
		return persistencyManager.exists(key);
	}
	
	@Override
	public Message get(Long key) throws InvalidKeyException, SQLException {
		return persistencyManager.get(key);
	}
	
	@Override
	public boolean delete(Long key) throws SQLException {
		final Message msg;
		try {
			msg = get(key);
			try {
				msg.accept(new IMessageVisitor<Object>() {
					@Override
					public Object visit(Message message) throws Exception {
						deleteMessage(msg.getId());
						return null;
					}
					
					@Override
					public Object visit(EventMessage event) throws Exception {
						removeEventAndAllData(event.getId());
						return null;
					}
				});
				return true;
			} catch (Exception e) {
				if (e instanceof SQLException) {
					throw (SQLException)e;
				} else {
					throw new AssertionError(e);
				}
			}
		} catch (InvalidKeyException e) {
			return false;
		}
	}
	
	private void deleteMessage(long messageKey) throws SQLException {
		persistencyManager.delete(messageKey);
	}
	
	/**
	 * Removes the event from the DB and all the corresponding data
	 * 
	 * @param eventKey
	 * @throws InvalidKeyException
	 * @throws SQLException
	 */
	private void removeEventAndAllData(long eventKey) throws InvalidKeyException, SQLException {
		Message msg = get(eventKey);
		if ((msg instanceof EventMessage) == false) {
			throw new InvalidKeyException(eventKey);
		}
		EventMessage event = (EventMessage)msg;
		RegistrationManager.getNewInstance(t).removeAllEventData(event);
		persistencyManager.delete(event.getId());
	}
	
	@Override
	public Collection<Message> getAll() throws SQLException {
		return persistencyManager.getAll();
	}
	
	/**
	 * Gets all the messages (not messages) that are within a certain a radius from a certain coordinate
	 * 
	 * @param center The center the radius is searched against
	 * @param distance The maximum distance from the coordinate
	 * @param since Only show messages new than this; use <code>null</code> to ignore
	 * @return A collection of all the messages in the radius
	 * @throws ArithmeticException If {@code distance < 0}
	 * @throws SQLException
	 */
	public Collection<Message> getAllMessagesInRadius(Location center, double distance, Long since)
			throws SQLException {
		return persistencyManager.getAllMessagesInRadius(center, distance, since);
	}
	
	/**
	 * Gets all the events that are within a certain a radius from a certain coordinate
	 * 
	 * @param center The center the radius is searched against
	 * @param distance The maximum distance from the coordinate
	 * @param since Only show messages new than this; use <code>null</code> to ignore
	 * @param includePastEvents If <code>true</code>, past events will be included; if <code>false</code> or
	 *            <code>null</code>, only events that have not yet take place will be included
	 * @return A collection of all the messages in the radius
	 * @throws ArithmeticException If {@code distance < 0}
	 * @throws SQLException
	 */
	public Collection<EventMessage> getAllEventsInRadius(Location center, double distance, Long since,
			Boolean includePastEvents) throws SQLException {
		return persistencyManager.getAllEventsInRadius(center, distance, since, includePastEvents == null ? false
				: includePastEvents);
	}
}
